﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LoA.Shared.Interfaces;
using Microsoft.Xna.Framework;

namespace LoA.CameraEngine.Entities {
	internal sealed class SecondPersonSpringCameraProcessor : CameraProcessorBase {
		public SecondPersonSpringCameraProcessor(Game game) {
			this.objectPosition = Vector3.UnitY * 2;
			this.objectDirection = Vector3.UnitX;

			this.UpdateViewMatrix( );
		}

		/// <summary>
		/// Position of object.
		/// </summary>
		public Vector3 ObjectPosition { get { return objectPosition; } set { objectPosition = value; } }
		private Vector3 objectPosition;

		/// <summary>
		/// Direction the object is facing.
		/// </summary>
		public Vector3 ObjectDirection { get { return objectDirection; } set { objectDirection = value; } }
		private Vector3 objectDirection;

		/// <summary>
		/// Object's Up vector.
		/// </summary>
		public Vector3 ObjectUp { get { return objectUp; } set { objectUp = value; } }
		private Vector3 objectUp = Vector3.Up;

		#region Camera physics (typically set when creating camera)
		/// <summary>
		/// Physics coefficient which controls the influence of the camera's position
		/// over the spring force. The stiffer the spring, the closer it will stay to
		/// the chased object.
		/// </summary>
		public float Stiffness {
			get { return stiffness; }
			set { stiffness = value; }
		}
		private float stiffness = 1800.0f;

		/// <summary>
		/// Physics coefficient which approximates internal friction of the spring.
		/// Sufficient damping will prevent the spring from oscillating infinitely.
		/// </summary>
		public float Damping {
			get { return damping; }
			set { damping = value; }
		}
		private float damping = 600.0f;

		/// <summary>
		/// Mass of the camera body. Heaver objects require stiffer springs with less
		/// damping to move at the same rate as lighter objects.
		/// </summary>
		public float Mass {
			get { return mass; }
			set { mass = value; }
		}
		private float mass = 50.0f;
		#endregion

		#region Desired camera positioning (set when creating camera or changing view)

		/// <summary>
		/// Desired camera position in the chased object's coordinate system.
		/// </summary>
		public Vector3 DesiredPositionOffset {
			get { return desiredPositionOffset; }
			set { desiredPositionOffset = value; }
		}
		private Vector3 desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);

		/// <summary>
		/// Desired camera position in world space.
		/// </summary>
		public Vector3 DesiredPosition {
			get {
				// Ensure correct value even if update has not been called this frame
				UpdateWorldPositions( );

				return desiredPosition;
			}
		}
		private Vector3 desiredPosition;

		/// <summary>
		/// Look at point in the chased object's coordinate system.
		/// </summary>
		public Vector3 LookAtOffset {
			get { return lookAtOffset; }
			set { lookAtOffset = value; }
		}
		private Vector3 lookAtOffset = new Vector3(0, 2.8f, 0);

		/// <summary>
		/// Look at point in world space.
		/// </summary>
		public Vector3 LookAt {
			get {
				// Ensure correct value even if update has not been called this frame
				UpdateWorldPositions( );

				return lookAt;
			}
		}
		private Vector3 lookAt;

		#endregion

		#region Current camera properties (updated by camera physics)

		/// <summary>
		/// Position of camera in world space.
		/// </summary>
		public Vector3 Position {
			get { return position; }
		}
		private Vector3 position;

		/// <summary>
		/// Velocity of camera.
		/// </summary>
		public Vector3 Velocity {
			get { return velocity; }
		}
		private Vector3 velocity;

		#endregion

		private void UpdateWorldPositions( ) {
			// Construct a matrix to transform from object space to worldspace
			Matrix transform = Matrix.Identity;
			transform.Forward = ObjectDirection;
			transform.Up = ObjectUp;
			transform.Right = Vector3.Cross(ObjectUp, ObjectDirection);

			// Calculate desired camera properties in world space
			desiredPosition = ObjectPosition +
				Vector3.TransformNormal(DesiredPositionOffset, transform);
			lookAt = ObjectPosition +
				Vector3.TransformNormal(LookAtOffset, transform);
		}

		private void UpdateViewMatrix( ) {
			this.ViewMatrix = Matrix.CreateLookAt(this.Position, this.LookAt, this.ObjectUp);
		}

		public override void Update(GameTime gameTime, IInput inputEngine) {
			if (gameTime == null)
				throw new ArgumentNullException("gameTime");

			UpdateWorldPositions( );

			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

			// Calculate spring force
			Vector3 stretch = position - desiredPosition;
			Vector3 force = -stiffness * stretch - damping * velocity;

			// Apply acceleration
			Vector3 acceleration = force / mass;
			velocity += acceleration * elapsed;

			// Apply velocity
			position += velocity * elapsed;

			this.UpdateViewMatrix( );
		}
	}
}
